
package com.friendfeed.api;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.AllClientPNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;

import com.friendfeed.api.internal.httpclient.GzipRequestInterceptor;
import com.friendfeed.api.internal.httpclient.GzipResponseInterceptor;

import com.google.gson.Gson;
import com.friendfeed.api.internal.gson.GsonFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.io.InputStream;

public class FriendFeedServiceImpl implements FriendFeedService {
	
	private static final String DEFAULT_CHARSET = HTTP.UTF_8;
	private static final String FRIENDFEED_API_BASE = "http://friendfeed.com/api/";
	
    private String nickname;
    private String remoteKey;
    private String service;
    private int start;
    private int num;
    private String via;
    private boolean useCompression = false;

    public FriendFeedServiceImpl() {
    }

    public FriendFeedServiceImpl(String nickname, String remoteKey) {
        this.nickname = nickname;
        this.remoteKey = remoteKey;
    }

    public void setService(String service) {
        this.service = service;
    }

    public void setStart(int start) {
        this.start = start;
    }

    public void setNum(int num) {
        this.num = num;
    }

    private HttpClient getHttpClient() {
        DefaultHttpClient client = new DefaultHttpClient();

		client.getParams().setParameter(AllClientPNames.USER_AGENT, "friendfeed-java-api");
		client.getParams().setIntParameter(AllClientPNames.CONNECTION_TIMEOUT, 15000 /* milliseconds */);
		client.getParams().setIntParameter(AllClientPNames.SO_TIMEOUT, 30000 /* milliseconds */);
    	
		if (this.getUseCompression()) {
			client.addRequestInterceptor(GzipRequestInterceptor.getInstance());
			client.addResponseInterceptor(GzipResponseInterceptor.getInstance());
		}
		
        if (nickname != null) {
            client.getCredentialsProvider().setCredentials(
                    new AuthScope("friendfeed.com", 80),
                    new UsernamePasswordCredentials(nickname, remoteKey)
            );
        }
        return (HttpClient) client;
    }

    protected void post(String url, Object... params) {
        sendHttpRequest("POST", url, params);
    }

    
    private void checkNickname(String nickname) {
        if (nickname == null) {
            throw new NullPointerException("Nickname cannot be null");
        }
    }

    private String fixupQueryUrl(String url) {
        StringBuilder sb = new StringBuilder(url);
        boolean queryAlreadyIncluded = url.contains("?");

        queryAlreadyIncluded = appendParam(sb, queryAlreadyIncluded, "service", service);

        if (start > 0) {
            queryAlreadyIncluded = appendParam(sb, queryAlreadyIncluded, "start", start);
        }

        if (num > 0) {
            appendParam(sb, queryAlreadyIncluded, "num", num);
        }

        return sb.toString();
    }

    private boolean appendParam(StringBuilder sb, boolean queryAlreadyIncluded, String name, Object value) {
        if (value != null) {
            if (!queryAlreadyIncluded) {
                sb.append("?");
            }

            try {
                if (queryAlreadyIncluded) {
                    sb.append("&");
                }

                sb.append(name).append("=").append(URLEncoder.encode(value.toString(), DEFAULT_CHARSET));
                queryAlreadyIncluded = true;
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException("UTF-8 not supported?!", e);
            }
        }

        return queryAlreadyIncluded;
    }

    private List<Entry> fetchEntries(String url) {
    	
    	EntryListResponse r = sendFriendFeedRequest("GET", url, EntryListResponse.class );
    	
    	return r.getEntries();
    	
    }


    protected <T> T sendFriendFeedRequest(String method, String url, Class<T> responseType, Object... params)
    {
    	
    	String json = sendHttpRequest( method, url, params );
    	
    	return fromJson(json, responseType);
    	
    }
    
    protected <T> T fromJson(String json, Class<T> clazz)
    {
    	Gson gson = GsonFactory.createGson();
    	
    	return gson.fromJson(json, clazz);
    }
    

    public List<Entry> fetchPublicFeed() {
        return fetchEntries(fixupQueryUrl(FRIENDFEED_API_BASE + "feed/public"));
    }

    public List<Entry> fetchUserFeed(String nickname) {
        checkNickname(nickname);

        return fetchEntries(fixupQueryUrl(FRIENDFEED_API_BASE + "feed/user/" + nickname));
    }

    public List<Entry> fetchHomeFeed() {
        return fetchEntries(fixupQueryUrl(FRIENDFEED_API_BASE + "feed/home"));
    }

	public List<Entry> fetchMultiUserFeed(String... nicknames) {
		return fetchMultiUserFeed(Arrays.asList(nicknames));
	}
   
    public List<Entry> fetchMultiUserFeed(List<String> nicknames) {
        return fetchEntries(fixupQueryUrl(FRIENDFEED_API_BASE + "feed/user?nickname=" + FriendFeedUtils.join(nicknames, ",")));
    }

    public void hide(String id) {
        post(FRIENDFEED_API_BASE + "entry/hide", "entry", id);
    }

    public void unhide(String id) {
        post(FRIENDFEED_API_BASE + "entry/hide", "entry", id, "unhide", "true");
    }

    public List<Entry> fetchRoomFeed(String roomNickname) {
        return fetchEntries(fixupQueryUrl(FRIENDFEED_API_BASE + "feed/room/" + roomNickname));
    }

    public List<Entry> fetchUserComments() {
        checkNickname(nickname);

        return fetchEntries(fixupQueryUrl(FRIENDFEED_API_BASE + "feed/user/" + nickname + "/comments"));
    }

    public User fetchProfile(String nickname) {
    	
            User user = sendFriendFeedRequest("GET", FRIENDFEED_API_BASE + "user/" + nickname + "/profile", User.class);
            return user;
    }

    public void addLike(String entryId) {
        post(FRIENDFEED_API_BASE + "like", 
        			"entry", entryId);
    }

    public void deleteLike(String entryId) {
        post(FRIENDFEED_API_BASE + "like/delete", 
        			"entry", entryId);
    }

    public Entry publishLink(String comment, String link) {

    	Entry e = sendFriendFeedRequest("POST", FRIENDFEED_API_BASE + "share", Entry.class, "title", comment, "link", link);

        return e;
        
    }
    
    /* Author: Davide Fucci
     *  14 Maggio 2009
     *  metodo per pubblicare una entry in una specifica stanza   
     */ 
    public Entry publishToRoom(String comment, String room) {

    	Entry e = sendFriendFeedRequest("POST", FRIENDFEED_API_BASE + "share", Entry.class, "title", comment, "room", room);

        return e;
        
    }
    

    public void addComment(String entryId, String body) {
        post(FRIENDFEED_API_BASE + "comment", 
        				"entry", entryId, 
        				"body", body);
    }

    public List<Entry> fetchEntryById(String... ids) {
    	
        StringBuilder sb = new StringBuilder(FRIENDFEED_API_BASE + "feed/entry?entry_id=");
        for (String id : ids) {
            sb.append(id).append(",");
        }
        sb.deleteCharAt(sb.length() - 1);

        return fetchEntries(fixupQueryUrl(sb.toString()));
    }

    public void setVia(String via) {
        this.via = via;
    }

    public void setUseCompression(boolean b) {
    	this.useCompression = b;
    }
    
    public boolean getUseCompression() {
    	return this.useCompression;
    }

    protected byte[] downloadBinary(String url)  {
    	
    	HttpGet get = new HttpGet(url);
    	
    	HttpClient client = getHttpClient();
    	
    	HttpResponse response = null;

    	try
    	{
    		response = client.execute(get);
    	
    		HttpEntity entity = response.getEntity();
    	
	    	if (entity == null) {
	    		throw new RuntimeException("nothing (?) at " + url);
	    	}
	    	else {
	    			return EntityUtils.toByteArray(entity);
	    	}
		}
		catch (Exception ex) {
			throw new RuntimeException(ex);
		}
		finally {
			// todo : something here?
		}
    	
    	
    }
    
	public byte[] fetch(MediaContent content) {
		
		String url = content.getUrl();
		
		if (url == null) {
			return new byte[0];
		}
		
		return downloadBinary(url);
	}

	public byte[] fetch(MediaThumbnail thumbnail) {
		
		String url = thumbnail.getUrl();
		
		if (url == null) {
			return new byte[0];
		}
		
		return downloadBinary(url);
	}

	
	protected String sendHttpRequest(String httpMethod, String url, Object... params)
	{
		

		HttpClient c = getHttpClient();
		
		HttpUriRequest request = null;
		
		
		if ("GET".equalsIgnoreCase(httpMethod))
		{

			String queryString = buildQueryString(params);
			
			url = url + queryString;
			
			request = new HttpGet(url);
			
		}
		else if ("POST".equalsIgnoreCase(httpMethod))
		{
			
			HttpPost post = new HttpPost(url);
			
			HttpEntity e = null;
			
			if ( (params.length == 2) 
					&& (params[0] instanceof InputStream) 
					&& (params[1] instanceof String)) { 
				
				String name = (String) params[1];
				
				MultipartEntity mpe = new MultipartEntity();
				mpe.addPart(name, new InputStreamBody((InputStream) params[0], "filename"));
				
				e = mpe;
				
			}
			else {
				
				List<NameValuePair> nvpairs = buildNameValuePairList(params);
				
		        try {
					e = new UrlEncodedFormEntity(nvpairs, DEFAULT_CHARSET);
				} 
		        catch (UnsupportedEncodingException ex) {
		        	throw new RuntimeException("encoding failed?", ex);
				}
			}
	        

			post.setEntity(e);
			
			request = post;
	        
		}
		else
		{
			throw new RuntimeException("unsupported method: " + httpMethod);
		}
	
		HttpResponse response = null;
		HttpEntity entity = null;
		
    	request.addHeader("Accept-Charset", DEFAULT_CHARSET);
    	

    	String responseBody = null;
    	
		try
		{
			response = c.execute(request);

			int responseCode = response.getStatusLine().getStatusCode();
			
			entity = response.getEntity();
		
			if (entity != null) {
				responseBody = EntityUtils.toString(entity);
			}
			
			 if (responseCode == 200) {
					return responseBody;
	         } else {
	                String errorCode = ERROR;

	                // todo : extract errorCode from responseBody
	                
	                if (responseCode == 401) {
	                    throw new AuthorizationRequiredException(url);
	                } else if (responseCode == 403) {
	                    throw new ForbiddenException(url);
	                } else {
	                    throw new UnexpectedResponseException(url, responseCode, errorCode);
	                }
	         }
			
		}
		catch (Exception ex)
		{
			throw new RuntimeException(ex);
		}
		finally
		{
			// todo : 
		}
		
	}

	
	private String buildQueryString(Object[] params)
	{
		StringBuffer query = new StringBuffer();
		
		if (params.length > 0)
		{
			query.append("?");
			
	        for (int i = 0; i < params.length; i = i + 2) {
	            if (!(params[i] instanceof String)) {
	                throw new FriendFeedServiceException("Odd numbered parameters must be of type String");
	            }

				String key = (String) params[i]; 
				query.append(key);
				query.append("=");
				query.append(encodeParameter( (String) params[i + 1]) );
				query.append("&");
	        }
	        
			
			if (query.charAt(query.length() - 1) == '&')
			{
				query.deleteCharAt(query.length() - 1);
			}
		}			
		
		return query.toString();
	}

	protected String decode(String s)
	{
		try
		{
			return URLDecoder.decode(s, DEFAULT_CHARSET);
		} 
		catch (UnsupportedEncodingException ex)
		{
			throw new RuntimeException(ex);
		}
	}
	
	protected String encodeParameter(String s)
	{
		try
		{
			return URLEncoder.encode(s, DEFAULT_CHARSET);
		}
		catch (Exception ex)
		{
			throw new RuntimeException(ex);
		}
	}

	protected List<NameValuePair> buildNameValuePairList(Object[] params)
	{
        List <NameValuePair> nvpairs = new ArrayList <NameValuePair>();
        
        for (int i = 0; i < params.length; i = i + 2) {
            if (!(params[i] instanceof String)) {
                throw new FriendFeedServiceException("Odd numbered parameters must be of type String");
            }

            nvpairs.add(new BasicNameValuePair(params[i].toString(), params[i + 1].toString()));
        }

        if (via != null) {
            nvpairs.add(new BasicNameValuePair("via", via));
        }


        return nvpairs;
	}

	public List<Entry> fetchUsersFriendsFeed(String nickname)
	{
        checkNickname(nickname);

        return fetchEntries(fixupQueryUrl(FRIENDFEED_API_BASE + "feed/user/" + nickname + "/friends"));
	}

	public List<Entry> search(String searchTerms)
	{
		if (searchTerms == null) {
			throw new IllegalArgumentException("searchTerms param is null");
		}
		
        return fetchEntries(fixupQueryUrl(FRIENDFEED_API_BASE + "feed/search?q=" + encodeParameter(searchTerms)));
	}

	public byte[] fetchProfilePicture(String user, Size sz)
	{
		return downloadBinary("http://friendfeed.com/" + user + "/picture?size=" + sz.toString());
	}

	public byte[] fetchRoomPicture(String roomName, Size sz)
	{
		return downloadBinary("http://friendfeed.com/rooms/" + roomName + "/picture?size=" + sz.toString());
	}

	public void updateProfilePicture(String user, InputStream image)
	{
		post(FRIENDFEED_API_BASE + "user/" + user + "/profile", image, "picture");
	}

	public List<Service> fetchServices()
	{
		ServiceListResponse r = sendFriendFeedRequest(
							"GET", 
							FRIENDFEED_API_BASE + "services", 
							ServiceListResponse.class);
		return r.getServices();
	}
	
}
